const isType = (data) => Object.prototype.toString.call(data);

class Vue {
  constructor(options) {
    // 初始化
    this.$options = options;
    this._data = this.$options.data();
    this.initData();
    this.initComputed();
    this.initWatch();
  }
  initData() {
    const data = this._data;
    const keys = Object.keys(data);
    // 将 data 的数据代理到 this 上可以通过 this 去访问
    // 注意这里 我们改变 this.xxx === this._data.xxx
    for (let i in keys) {
      Object.defineProperty(this, keys[i], {
        get() {
          return data[keys[i]];
        },
        set(value) {
          data[keys[i]] = value;
        },
      });
    }
    // 我们将 _data 注册订阅
    observer(data);
  }

  initComputed() {
    const computed = this.$options.computed;
    if (!computed) return;
    const keys = Object.keys(computed);
    for (let i in keys) {
      const key = keys[i];
      const watcher = new Watcher(this, computed[key], function () {});
      Object.defineProperty(this, key, {
        get: function computedGetter() {
          watcher.get();
          return watcher.value;
        },
        set: function computedSetter() {
          console.error("不能改变");
        },
      });
    }
  }

  initWatch() {
    const watch = this.$options.watch;
    if (!watch) return;
    const keys = Object.keys(watch);
    for (let i in keys) {
      new Watcher(this, keys[i], watch[keys[i]]);
    }
  }

  $watch(exp, cb) {
    new Watcher(this, exp, cb);
  }

  $set(target, key, value) {
    defineReactive(target, key, value);
    target.__ob__.dep.nothify();
  }
}
// 闭环
function observer(data) {
  const type = isType(data);
  if (type === "[object Object]" || type === "[object Array]") {
    // 如果data.__ob__ 已经存在说明已经订阅过了
    if (data.__ob__) return data.__ob__;

    return new Observer(data);
  }
}

class Observer {
  constructor(data) {
    this.dep = new Dep();
    if (Array.isArray(data)) {
      data.__proto__ = arrayMethods;
      this.observerArray(data);
    } else {
      this.wilk(data);
    }
    Object.defineProperty(data, "__ob__", {
      value: this,
      enumerable: false,
    });
  }

  wilk(data) {
    const keys = Object.keys(data);
    for (let i in keys) {
      defineReactive(data, keys[i], data[keys[i]]);
    }
  }

  observerArray(arr) {
    arr.forEach((item) => {
      observer(item);
    });
  }
}

function defineReactive(obj, key, value) {
  // 创建收集器
  const dep = new Dep();
  const childOb = observer(obj[key]);
  Object.defineProperty(obj, key, {
    get: function reactiveGetter() {
      // 订阅
      dep.depend();
      if (childOb) childOb.dep.depend();
      return value;
    },
    set: function reactiveSetter(val) {
      if (val == value) return;
      // 发布
      dep.nothify();
      value = val;
    },
  });
}

class Dep {
  constructor() {
    this.subs = [];
  }

  depend() {
    // 订阅watcher
    if (Dep.target) {
      this.subs.push(Dep.target);
    }
  }

  nothify() {
    // 执行订阅
    this.subs.forEach((watcher) => {
      watcher.run();
    });
  }
}

const watcherArr = [];
let watcherId = 0;
class Watcher {
  constructor(vm, exp, cb) {
    this.vm = vm;
    this.exp = exp;
    this.cb = cb;
    this.id = ++watcherId;
    this.get();
  }

  get() {
    // 触发监听的 get 之后 会执行 Dep 中的订阅
    // 将 this 登上舞台 又下去
    Dep.target = this;
    if (isType(this.exp) === "[object Function]") {
      this.value = this.exp.call(this.vm);
    } else {
      this.value = this.vm[this.exp];
    }
    Dep.target = null;
  }
  run() {
    // 运行传入的函数 将watch放入队列的最后面 如果 watch 存在队列中 直接退出
    if (watcherArr.indexOf(this.id) !== -1) return;
    watcherArr.push(this.id);
    const index = watcherArr.length - 1;

    Promise.resolve().then(() => {
      // 执行完 将当前 watch 从队列中清除
      this.cb.call(this.vm);
      watcherArr.splice(index, 1);
    });
  }
}

// 改写数组的原型
const arrayMethods = Object.create(Array.prototype);
const mutMethods = ["push", "pop"];
mutMethods.forEach((method) => {
  arrayMethods[method] = function (...args) {
    const result = Array.prototype[method].apply(this, args);
    if (method === "push") {
      this.__ob__.observerArray(args);
    }
    this.__ob__.dep.nothify();
    return result;
  };
});
